agora inbox for [email protected]
help / color / mirror / Atom feedIDEA: Multi-master replication possible through spread (or even master-slave)...
817+ messages / 4 participants
[nested] [flat]
* IDEA: Multi-master replication possible through spread (or even master-slave)...
@ 2001-07-21 03:32 Sean Chittenden <[email protected]>
0 siblings, 1 reply; 817+ messages in thread
From: Sean Chittenden @ 2001-07-21 03:32 UTC (permalink / raw)
To: pgsql-hackers
Has anyone here thought about using the spread libraries for WAL
replication amongst mutliple hosts? With this library I think it'd be
possible to have a multi-master replication system...
http://www.spread.org/
I'm not familiar enough with the guts of postgres to be able to
impliment this (yet), but thought that it might be something for someone
else to look into. At the very least, check out the libs, they're very
impressive and I've had good luck with them in the past. If push comes
to shove, in a few months I may write a few ruby+spread utils to do
this, but we'll see... -sc
--
Sean Chittenden
^ permalink raw reply [nested|flat] 817+ messages in thread
* Re: IDEA: Multi-master replication possible through spread (or even master-slave)...
@ 2001-07-21 05:01 Darren Johnson <[email protected]>
parent: Sean Chittenden <[email protected]>
0 siblings, 1 reply; 817+ messages in thread
From: Darren Johnson @ 2001-07-21 05:01 UTC (permalink / raw)
To: Sean Chittenden <[email protected]>; +Cc: pgsql-hackers
Sean Chittenden wrote:
> Has anyone here thought about using the spread libraries for WAL
> replication amongst mutliple hosts? With this library I think it'd be
> possible to have a multi-master replication system..
Yes, there is some work being done to use Spread as the group
communication system
for Postgres-R, but we are just getting started with this software.
Using a group
communication system to establish total order messages is one of the
basic principles for
synchronous multi-master replication with Postgres-R. Currently
Ensemble (form Cornell
University) is used, but Spread looks to be more robust and it appears
to be supported
on most if not all of the PostgreSQL supported platforms.
It's very cool to see positive testimony for Spread, and I hope I will
feel the same way
as I become more familiar with it.
Darren
>
^ permalink raw reply [nested|flat] 817+ messages in thread
* Re: IDEA: Multi-master replication possible through spread (or even master-slave)...
@ 2001-07-21 05:12 Sean Chittenden <[email protected]>
parent: Darren Johnson <[email protected]>
0 siblings, 1 reply; 817+ messages in thread
From: Sean Chittenden @ 2001-07-21 05:12 UTC (permalink / raw)
To: Darren Johnson <[email protected]>; +Cc: pgsql-hackers
Howdy. Darren, I'd reply in person, but there are issues with
your mail account. ;~) At anyrate, is there a mailing list that the
Postgres-R development is happening on so that I could drop in and
either listen/contribute? Thanks. -sc
<[email protected]>:
63.136.234.38 does not like recipient.
Remote host said: 550 <[email protected]>... User unknown
Giving up on 63.136.234.38.
--- Below this line is a copy of the message.
Return-Path: <[email protected]>
Received: (qmail 9478 invoked by uid 1001); 21 Jul 2001 05:09:21 -0000
Date: Fri, 20 Jul 2001 22:09:21 -0700
From: Sean Chittenden <[email protected]>
To: Darren Johnson <[email protected]>
Subject: Re: [HACKERS] IDEA: Multi-master replication possible through
spread
+(or even master-slave)...
Message-ID: <[email protected]>
References: <[email protected]>
+<[email protected]>
Mime-Version: 1.0
Content-Type: multipart/signed; micalg=pgp-sha1;
protocol="application/pgp-signature";
boundary="1ppIqr1kl39GnwQx"
Content-Disposition: inline
In-Reply-To: <[email protected]>; from
"[email protected]"
+on Sat, Jul 21, 2001 at = 01:01:58AM
On Sat, Jul 21, 2001 at 01:01:58AM -0400, Darren Johnson wrote:
> Delivered-To: [email protected]
> Date: Sat, 21 Jul 2001 01:01:58 -0400
> From: Darren Johnson <[email protected]>
> User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; m18) Gecko/20010131 Netscape6/6.01
> X-Accept-Language: en
> To: Sean Chittenden <[email protected]>
> CC: [email protected]
> Subject: Re: [HACKERS] IDEA: Multi-master replication possible through spread (or even master-slave)...
>
> Sean Chittenden wrote:
>
> > Has anyone here thought about using the spread libraries for WAL
> > replication amongst mutliple hosts? With this library I think it'd be
> > possible to have a multi-master replication system..
>
> Yes, there is some work being done to use Spread as the group
> communication system
> for Postgres-R, but we are just getting started with this software.
> Using a group
> communication system to establish total order messages is one of the
> basic principles for
> synchronous multi-master replication with Postgres-R. Currently
> Ensemble (form Cornell
> University) is used, but Spread looks to be more robust and it appears
> to be supported
> on most if not all of the PostgreSQL supported platforms.
>
> It's very cool to see positive testimony for Spread, and I hope I will
> feel the same way
> as I become more familiar with it.
--
Sean Chittenden
^ permalink raw reply [nested|flat] 817+ messages in thread
* Re: IDEA: Multi-master replication possible through spread (or even master-slave)...
@ 2001-07-21 05:58 Darren Johnson <[email protected]>
parent: Sean Chittenden <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Darren Johnson @ 2001-07-21 05:58 UTC (permalink / raw)
To: Sean Chittenden <[email protected]>; +Cc: pgsql-hackers
Sure. The mailing list is
http://www.greatbridge.org/mailman/listinfo/pgreplication-general
It's not only for Postgres-R, but any PostgreSQL
replication ideas, discussions, or projects.
Feel free to listen or contribute.
Darren
BTW: My apologies for the email issues. Should be fixed now.
Sean Chittenden wrote:
> Howdy. Darren, I'd reply in person, but there are issues with
> your mail account. ;~) At anyrate, is there a mailing list that the
> Postgres-R development is happening on so that I could drop in and
> either listen/contribute? Thanks. -sc
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 817+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 817+ messages in thread
end of thread, other threads:[~2025-07-18 14:52 UTC | newest]
Thread overview: 817+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2001-07-21 03:32 IDEA: Multi-master replication possible through spread (or even master-slave)... Sean Chittenden <[email protected]>
2001-07-21 05:01 ` Darren Johnson <[email protected]>
2001-07-21 05:12 ` Sean Chittenden <[email protected]>
2001-07-21 05:58 ` Darren Johnson <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox